上一篇我們是在沒有 PHPUnit 的幫助之下進行程式開發,今天我們就改用 PHPUnit 來開發看會是什麼樣的情況吧
PHPUnit 最麻煩的步驟就是安裝 PHPUnit 了,所以我們就接著來解決就麻煩的步驟吧!打開 cli 後輸入以下指令
composer init
接著看到以下的訊息後依照『<--』指示輸入(沒特別標示的直接輸入 enter 即可)
❯ composer init
Welcome to the Composer config generator
This command will guide you through creating your composer.json config.
Package name (<vendor>/<name>) [recca0120/ithome-30]:
Description []:
Author [recca0120 <recca0120@gmail.com>, n to skip]:
Minimum Stability []:
Package Type (e.g. library, project, metapackage, composer-plugin) []:
License []:
Define your dependencies.
Would you like to define your dependencies (require) interactively [yes]? no # <-- 輸入 no
Would you like to define your dev dependencies (require-dev) interactively [yes]? yes # <-- 輸入 yes
Search for a package: phpunit # <-- 輸入 phpunit
Info from https://repo.packagist.org: #StandWithUkraine
Found 15 packages matching phpunit
[0] phpunit/phpunit
[1] phpunit/php-timer
[2] phpunit/php-text-template
[3] phpunit/php-file-iterator
[4] phpunit/php-code-coverage
[5] phpunit/phpunit-mock-objects Abandoned. No replacement was suggested.
[6] symfony/phpunit-bridge
[7] jean85/pretty-package-versions
[8] phpunit/php-invoker
[9] phpunit/php-token-stream Abandoned. No replacement was suggested.
[10] johnkary/phpunit-speedtrap
[11] phpstan/phpstan-phpunit
[12] brianium/paratest
[13] yoast/phpunit-polyfills
[14] spatie/phpunit-snapshot-assertions
Enter package # to add, or the complete package name if it is not listed: 0 # <-- 輸入 0
Enter the version constraint to require (or leave blank to use the latest version):
Using version ^10.3 for phpunit/phpunit
Search for a package:
Add PSR-4 autoload mapping? Maps namespace "Recca0120\Ithome30" to the entered relative path. [src/, n to skip]:
{
"name": "recca0120/ithome-30",
"require-dev": {
"phpunit/phpunit": "^10.3"
},
"autoload": {
"psr-4": {
"Recca0120\\Ithome30\\": "src/"
}
},
"authors": [
{
"name": "recca0120",
"email": "recca0120@gmail.com"
}
],
"require": {}
}
Do you confirm generation [yes]?
Would you like to install dependencies now [yes]?
Loading composer repositories with package information
Updating dependencies
Lock file operations: 26 installs, 0 updates, 0 removals
- Locking myclabs/deep-copy (1.11.1)
- Locking nikic/php-parser (v4.17.1)
- Locking phar-io/manifest (2.0.3)
- Locking phar-io/version (3.2.1)
- Locking phpunit/php-code-coverage (10.1.5)
- Locking phpunit/php-file-iterator (4.1.0)
- Locking phpunit/php-invoker (4.0.0)
- Locking phpunit/php-text-template (3.0.1)
- Locking phpunit/php-timer (6.0.0)
- Locking phpunit/phpunit (10.3.4)
- Locking sebastian/cli-parser (2.0.0)
- Locking sebastian/code-unit (2.0.0)
- Locking sebastian/code-unit-reverse-lookup (3.0.0)
- Locking sebastian/comparator (5.0.1)
- Locking sebastian/complexity (3.0.1)
- Locking sebastian/diff (5.0.3)
- Locking sebastian/environment (6.0.1)
- Locking sebastian/exporter (5.0.1)
- Locking sebastian/global-state (6.0.1)
- Locking sebastian/lines-of-code (2.0.1)
- Locking sebastian/object-enumerator (5.0.0)
- Locking sebastian/object-reflector (3.0.0)
- Locking sebastian/recursion-context (5.0.0)
- Locking sebastian/type (4.0.0)
- Locking sebastian/version (4.0.1)
- Locking theseer/tokenizer (1.2.1)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 26 installs, 0 updates, 0 removals
- Installing sebastian/version (4.0.1): Extracting archive
- Installing sebastian/type (4.0.0): Extracting archive
- Installing sebastian/recursion-context (5.0.0): Extracting archive
- Installing sebastian/object-reflector (3.0.0): Extracting archive
- Installing sebastian/object-enumerator (5.0.0): Extracting archive
- Installing sebastian/global-state (6.0.1): Extracting archive
- Installing sebastian/exporter (5.0.1): Extracting archive
- Installing sebastian/environment (6.0.1): Extracting archive
- Installing sebastian/diff (5.0.3): Extracting archive
- Installing sebastian/comparator (5.0.1): Extracting archive
- Installing sebastian/code-unit (2.0.0): Extracting archive
- Installing sebastian/cli-parser (2.0.0): Extracting archive
- Installing phpunit/php-timer (6.0.0): Extracting archive
- Installing phpunit/php-text-template (3.0.1): Extracting archive
- Installing phpunit/php-invoker (4.0.0): Extracting archive
- Installing phpunit/php-file-iterator (4.1.0): Extracting archive
- Installing theseer/tokenizer (1.2.1): Extracting archive
- Installing nikic/php-parser (v4.17.1): Extracting archive
- Installing sebastian/lines-of-code (2.0.1): Extracting archive
- Installing sebastian/complexity (3.0.1): Extracting archive
- Installing sebastian/code-unit-reverse-lookup (3.0.0): Extracting archive
- Installing phpunit/php-code-coverage (10.1.5): Extracting archive
- Installing phar-io/version (3.2.1): Extracting archive
- Installing phar-io/manifest (2.0.3): Extracting archive
- Installing myclabs/deep-copy (1.11.1): Extracting archive
- Installing phpunit/phpunit (10.3.4): Extracting archive
2 package suggestions were added by new dependencies, use `composer suggest` to see details.
Generating autoload files
23 packages you are using are looking for funding.
Use the `composer fund` command to find out more!
No security vulnerability advisories found
PSR-4 autoloading configured. Use "namespace Recca0120\Ithome30;" in src/
Include the Composer autoloader with: require 'vendor/autoload.php';
ithome-30 on main [?] via 🐘 v8.1.22 took 34s
❯
再接著輸入
vendor/bin/phpunit --generate-configuration
得到以下訊息(全部按 enter 照預設值即可)
PHPUnit 10.3.4 by Sebastian Bergmann and contributors.
Generating phpunit.xml in /Users/recca0120/Sites/ithome-30
Bootstrap script (relative to path shown above; default: vendor/autoload.php):
Tests directory (relative to path shown above; default: tests):
Source directory (relative to path shown above; default: src):
Cache directory (relative to path shown above; default: .phpunit.cache):
Generated phpunit.xml in /Users/recca0120/Sites/ithome-30.
Make sure to exclude the .phpunit.cache directory from version control.
使用 git 的話記得將 .phpunit.cache 加入 .gitignore
再修改產出的 phpunit.xml,將 requireCoverageMetadata 改為 false。
完整的 phpunit.xml 如下
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
requireCoverageMetadata="false"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<source restrictDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
最後在 composer.json 加入以下的內容
{
// ...
"autoload-dev": {
"psr-4": {
"Recca0120\\Ithome30\\tests\\": "tests/"
}
},
}
composer.json 的完整內容為
{
"name": "recca0120/ithome-30",
"require-dev": {
"phpunit/phpunit": "^10.3"
},
"autoload": {
"psr-4": {
"Recca0120\\Ithome30\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Recca0120\\Ithome30\\Tests\\": "tests/"
}
},
"authors": [
{
"name": "recca0120",
"email": "recca0120@gmail.com"
}
],
"require": {}
}
至此終於完成最麻煩的步驟了
打開 tests/HelloWorldTest.php 輸入以下內容
<?php
// tests/HelloWroldTest.php
namespace Recca0120\Ithome30\Tests;
use PHPUnit\Framework\TestCase;
use Recca0120\Ithome30\HelloWorld;
class HelloWorldTest extends TestCase
{
public function test_hello_world()
{
self::assertEquals('hello world!!!', (new HelloWorld())->greeting());
}
}
接著撰寫 src/HelloWorld.php
<?php
// src/HelloWrold.php
namespace Recca0120\Ithome30;
class HelloWorld
{
public function greeting()
{
return 'hello world';
}
}
最後執行 vendor/bin/phpunit
❯ vendor/bin/phpunit
PHPUnit 10.3.4 by Sebastian Bergmann and contributors.
Runtime: PHP 8.1.22
Configuration: /Users/recca0120/Sites/ithome-30/phpunit.xml
F 1 / 1 (100%)
Time: 00:00.010, Memory: 8.00 MB
There was 1 failure:
1) Recca0120\Ithome30\Tests\HelloWorldTest::test_hello_world
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'hello world!!!'
+'hello world'
/Users/recca0120/Sites/ithome-30/tests/HelloWorldTest.php:12
FAILURES!
Tests: 1, Assertions: 1, Failures: 1.
這時會看到以上的訊息,我們可以很清楚知道輸出結果不如我們預期(程式確定是故意寫錯的)
我們這可以依照錯誤訊息進行 debug,所以我們只需要再把 src/HelloWorld.php 改為
<?php
// src/HelloWrold.php
namespace Recca0120\Ithome30;
class HelloWorld
{
public function greeting()
{
return 'hello world!!!';
}
}
再執行一次 vendor/bin/phpunit
會得到以下的結果
❯ vendor/bin/phpunit
PHPUnit 10.3.4 by Sebastian Bergmann and contributors.
Runtime: PHP 8.1.22
Configuration: /Users/recca0120/Sites/ithome-30/phpunit.xml
. 1 / 1 (100%)
Time: 00:00.004, Memory: 8.00 MB
OK (1 test, 1 assertion)
最後我們再把 index.php 再做點調整
<?php
// index.php
require_once(__DIR__ . '/vendor/autoload.php');
use Recca0120\Ithome30\HelloWorld;
var_dump("hello world!!!" === (new HelloWorld())->greeting());
至此我們已經完成第一個簡易的單元測試,更重要的是當程式輸出結果不正確時會比上一篇土炮式測試的訊息提示來得更完整,而且我們是在產出 production code 過程中,順便也完成了一個測試案例!而這樣的撰寫方法不就是測試先行嗎?
對!必須得多寫 Test,但這些 code 我們可以讓編輯器或 IDE 來替我們產出的,所以下一篇開始會介紹一些 VSCode 的 extension,讓整個開發流程更為順暢
使用瀏覽器來開發程式和土炮式單元測試的開發方式幾乎是一模一樣的,土炮式單元測試和 PHPUnit 開發也幾乎是一模一樣的,所以是用瀏覽器開發等於用 PHPUnit 開發啊,我們要開始使用 PHPUnit 來開發要做的也只有強迫自己改變開發方式啊